using System.Diagnostics;
using System.Xml;

namespace Vibstudio.X4NET.Xml.Comparision
{
    internal class XmlHash
    {
        private const string DELIMITER = "\0x01";

        // Fields
        private bool _bIgnoreChildOrder;

        private bool _bIgnoreComments;

        private bool _bIgnoreDtd;

        private bool _bIgnoreNamespaces;

        private bool _bIgnorePI;

        private bool _bIgnorePrefixes;

        private bool _bIgnoreWhitespace;

        private bool _bIgnoreXmlDecl;

        // Constructor

        internal XmlHash(XmlDiff xmlDiff)
        {
            // set flags
            _bIgnoreChildOrder = xmlDiff.IgnoreChildOrder;
            _bIgnoreComments = xmlDiff.IgnoreComments;
            _bIgnorePI = xmlDiff.IgnorePI;
            _bIgnoreWhitespace = xmlDiff.IgnoreWhitespace;
            _bIgnoreNamespaces = xmlDiff.IgnoreNamespaces;
            _bIgnorePrefixes = xmlDiff.IgnorePrefixes;
            _bIgnoreXmlDecl = xmlDiff.IgnoreXmlDecl;
            _bIgnoreDtd = xmlDiff.IgnoreDtd;
        }

        internal XmlHash()
        {
        }

        // Methods

        internal ulong ComputeHash(XmlNode node, XmlDiffOptions options)
        {
            _bIgnoreChildOrder = (((int)options & (int)(XmlDiffOptions.IgnoreChildOrder)) > 0);
            _bIgnoreComments = (((int)options & (int)(XmlDiffOptions.IgnoreComments)) > 0);
            _bIgnorePI = (((int)options & (int)(XmlDiffOptions.IgnorePI)) > 0);
            _bIgnoreWhitespace = (((int)options & (int)(XmlDiffOptions.IgnoreWhitespace)) > 0);
            _bIgnoreNamespaces = (((int)options & (int)(XmlDiffOptions.IgnoreNamespaces)) > 0);
            _bIgnorePrefixes = (((int)options & (int)(XmlDiffOptions.IgnorePrefixes)) > 0);
            _bIgnoreXmlDecl = (((int)options & (int)(XmlDiffOptions.IgnoreXmlDecl)) > 0);
            _bIgnoreDtd = (((int)options & (int)(XmlDiffOptions.IgnoreDtd)) > 0);

            return ComputeHash(node);
        }

        internal ulong ComputeHash(XmlNode node)
        {
            switch (node.NodeType)
            {
                case XmlNodeType.Document:
                    return ComputeHashXmlDocument((XmlDocument)node);
                case XmlNodeType.DocumentFragment:
                    return ComputeHashXmlFragment((XmlDocumentFragment)node);
                default:
                    return ComputeHashXmlNode(node);
            }
        }

        private ulong ComputeHashXmlDocument(XmlDocument doc)
        {
            var ha = new HashAlgorithm();
            HashDocument(ha);
            ComputeHashXmlChildren(ha, doc);
            return ha.Hash;
        }

        private ulong ComputeHashXmlFragment(XmlDocumentFragment frag)
        {
            var ha = new HashAlgorithm();
            ComputeHashXmlChildren(ha, frag);
            return ha.Hash;
        }

        internal ulong ComputeHashXmlDiffDocument(XmlDiffDocument doc)
        {
            var ha = new HashAlgorithm();
            HashDocument(ha);
            ComputeHashXmlDiffChildren(ha, doc);
            return ha.Hash;
        }

        internal ulong ComputeHashXmlDiffElement(XmlDiffElement el)
        {
            var ha = new HashAlgorithm();
            HashElement(ha, el.LocalName, el.Prefix, el.NamespaceURI);
            ComputeHashXmlDiffAttributes(ha, el);
            ComputeHashXmlDiffChildren(ha, el);
            return ha.Hash;
        }

        private void ComputeHashXmlDiffAttributes(HashAlgorithm ha, XmlDiffElement el)
        {
            var attrCount = 0;
            ulong attrHashAll = 0;
            var curAttrOrNs = el._attributes;
            while (curAttrOrNs != null)
            {
                attrHashAll += curAttrOrNs.HashValue;
                attrCount++;
                curAttrOrNs = (XmlDiffAttributeOrNamespace)curAttrOrNs._nextSibling;
            }

            if (attrCount > 0)
            {
                ha.AddULong(attrHashAll);
                ha.AddInt(attrCount);
            }
        }

        private void ComputeHashXmlDiffChildren(HashAlgorithm ha, XmlDiffParentNode parent)
        {
            var childrenCount = 0;
            if (_bIgnoreChildOrder)
            {
                ulong totalHash = 0;
                var curChild = parent.FirstChildNode;
                while (curChild != null)
                {
                    Debug.Assert(!(curChild is XmlDiffAttributeOrNamespace));
                    Debug.Assert(curChild.HashValue != 0);

                    totalHash += curChild.HashValue;
                    childrenCount++;
                    curChild = curChild._nextSibling;
                }
                ha.AddULong(totalHash);
            }
            else
            {
                var curChild = parent.FirstChildNode;
                while (curChild != null)
                {
                    Debug.Assert(!(curChild is XmlDiffAttributeOrNamespace));
                    Debug.Assert(curChild.HashValue != 0);

                    ha.AddULong(curChild.HashValue);
                    childrenCount++;
                    curChild = curChild._nextSibling;
                }
            }

            if (childrenCount != 0)
            {
                ha.AddInt(childrenCount);
            }
        }

        private void ComputeHashXmlChildren(HashAlgorithm ha, XmlNode parent)
        {
            var el = parent as XmlElement;
            if (el != null)
            {
                ulong attrHashSum = 0;
                var attrsCount = 0;
                var attrs = ((XmlElement)parent).Attributes;
                for (var i = 0; i < attrs.Count; i++)
                {
                    var attr = (XmlAttribute)attrs.Item(i);

                    ulong hashValue;

                    // default namespace def
                    if (attr.LocalName == "xmlns" && attr.Prefix == string.Empty)
                    {
                        if (_bIgnoreNamespaces)
                        {
                            continue;
                        }
                        hashValue = HashNamespace(string.Empty, attr.Value);
                    }
                    // namespace def
                    else if (attr.Prefix == "xmlns")
                    {
                        if (_bIgnoreNamespaces)
                        {
                            continue;
                        }
                        hashValue = HashNamespace(attr.LocalName, attr.Value);
                    }
                    // attribute
                    else
                    {
                        if (_bIgnoreWhitespace)
                        {
                            hashValue = HashAttribute(attr.LocalName, attr.Prefix, attr.NamespaceURI, XmlDiff.NormalizeText(attr.Value));
                        }
                        else
                        {
                            hashValue = HashAttribute(attr.LocalName, attr.Prefix, attr.NamespaceURI, attr.Value);
                        }
                    }

                    Debug.Assert(hashValue != 0);

                    attrsCount++;
                    attrHashSum += hashValue;
                }

                if (attrsCount != 0)
                {
                    ha.AddULong(attrHashSum);
                    ha.AddInt(attrsCount);
                }
            }

            var childrenCount = 0;
            if (_bIgnoreChildOrder)
            {
                ulong totalHashSum = 0;
                var curChild = parent.FirstChild;
                while (curChild != null)
                {
                    var hashValue = ComputeHashXmlNode(curChild);
                    if (hashValue != 0)
                    {
                        totalHashSum += hashValue;
                        childrenCount++;
                    }
                    curChild = curChild.NextSibling;
                }
                ha.AddULong(totalHashSum);
            }
            else
            {
                var curChild = parent.FirstChild;
                while (curChild != null)
                {
                    var hashValue = ComputeHashXmlNode(curChild);
                    if (hashValue != 0)
                    {
                        ha.AddULong(hashValue);
                        childrenCount++;
                    }
                    curChild = curChild.NextSibling;
                }
            }
            if (childrenCount != 0)
            {
                ha.AddInt(childrenCount);
            }
        }

        private ulong ComputeHashXmlNode(XmlNode node)
        {
            switch (node.NodeType)
            {
                case XmlNodeType.Element:
                {
                    var el = (XmlElement)node;
                    var ha = new HashAlgorithm();

                    HashElement(ha, el.LocalName, el.Prefix, el.NamespaceURI);
                    ComputeHashXmlChildren(ha, el);

                    return ha.Hash;
                }
                case XmlNodeType.Attribute:
                    Debug.Assert(false);
                    return 0;

                case XmlNodeType.Whitespace:
                    return 0;

                case XmlNodeType.SignificantWhitespace:
                    if (!_bIgnoreWhitespace)
                    {
                        goto case XmlNodeType.Text;
                    }
                    return 0;
                case XmlNodeType.Comment:
                    if (!_bIgnoreComments)
                    {
                        return HashCharacterNode(XmlNodeType.Comment, ((XmlCharacterData)node).Value);
                    }
                    return 0;
                case XmlNodeType.Text:
                {
                    var cd = (XmlCharacterData)node;
                    if (_bIgnoreWhitespace)
                    {
                        return HashCharacterNode(cd.NodeType, XmlDiff.NormalizeText(cd.Value));
                    }
                    return HashCharacterNode(cd.NodeType, cd.Value);
                }
                case XmlNodeType.CDATA:
                {
                    var cd = (XmlCharacterData)node;
                    return HashCharacterNode(cd.NodeType, cd.Value);
                }
                case XmlNodeType.ProcessingInstruction:
                {
                    if (_bIgnorePI)
                    {
                        return 0;
                    }

                    var pi = (XmlProcessingInstruction)node;
                    return HashPI(pi.Target, pi.Value);
                }
                case XmlNodeType.EntityReference:
                {
                    var er = (XmlEntityReference)node;
                    return HashER(er.Name);
                }
                case XmlNodeType.XmlDeclaration:
                {
                    if (_bIgnoreXmlDecl)
                    {
                        return 0;
                    }
                    var decl = (XmlDeclaration)node;
                    return HashXmlDeclaration(XmlDiff.NormalizeXmlDeclaration(decl.Value));
                }
                case XmlNodeType.DocumentType:
                {
                    if (_bIgnoreDtd)
                    {
                        return 0;
                    }
                    var docType = (XmlDocumentType)node;
                    return HashDocumentType(docType.Name, docType.PublicId, docType.SystemId, docType.InternalSubset);
                }
                case XmlNodeType.DocumentFragment:
                    return 0;
                default:
                    Debug.Assert(false);
                    return 0;
            }
        }

        private void HashDocument(HashAlgorithm ha)
        {
            // Intentionally empty
        }

        internal void HashElement(HashAlgorithm ha, string localName, string prefix, string ns)
        {
            ha.AddString((int)(XmlNodeType.Element) +
                         DELIMITER +
                         ((_bIgnoreNamespaces || _bIgnorePrefixes) ? string.Empty : prefix) +
                         DELIMITER +
                         (_bIgnoreNamespaces ? string.Empty : ns) +
                         DELIMITER +
                         localName);
        }

        internal ulong HashAttribute(string localName, string prefix, string ns, string value)
        {
            return HashAlgorithm.GetHash((int)XmlNodeType.Attribute +
                                         DELIMITER +
                                         ((_bIgnoreNamespaces || _bIgnorePrefixes) ? string.Empty : prefix) +
                                         DELIMITER +
                                         (_bIgnoreNamespaces ? string.Empty : ns) +
                                         DELIMITER +
                                         localName +
                                         DELIMITER +
                                         value);
        }

        internal ulong HashNamespace(string prefix, string ns)
        {
            Debug.Assert(!_bIgnoreNamespaces);

            return HashAlgorithm.GetHash((int)XmlDiffNodeType.Namespace +
                                         DELIMITER +
                                         (_bIgnorePrefixes ? string.Empty : prefix) +
                                         DELIMITER +
                                         ns);
        }

        internal ulong HashCharacterNode(XmlNodeType nodeType, string value)
        {
            return HashAlgorithm.GetHash(((int)nodeType) +
                                         DELIMITER +
                                         value);
        }

        internal ulong HashPI(string target, string value)
        {
            return HashAlgorithm.GetHash(((int)XmlNodeType.ProcessingInstruction) +
                                         DELIMITER +
                                         target +
                                         DELIMITER +
                                         value);
        }

        internal ulong HashER(string name)
        {
            return HashAlgorithm.GetHash(((int)XmlNodeType.EntityReference) +
                                         DELIMITER +
                                         name);
        }

        internal ulong HashXmlDeclaration(string value)
        {
            return HashAlgorithm.GetHash(((int)XmlNodeType.XmlDeclaration) +
                                         DELIMITER +
                                         value);
        }

        internal ulong HashDocumentType(string name, string publicId, string systemId, string subset)
        {
            return HashAlgorithm.GetHash(((int)XmlNodeType.DocumentType) +
                                         DELIMITER +
                                         name +
                                         DELIMITER +
                                         publicId +
                                         DELIMITER +
                                         systemId +
                                         DELIMITER +
                                         subset);
        }
    }
}